You don't know js ( this & object prototypes ) 学习笔记

第一章:this是什么?

在学习使用this之前我们首先要清楚两点:

1.this既不是函数自身的引用,也不是函数词法作用域的引用。

2.this其实是在函数调用时才建立的一个绑定,它的指向与函数声明的位置无关,而与函数调用的位置有关。

为什么要用this

总结:用this可以简化代码,不用利用频繁传参的方式来实现调用。

两段代码对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + identify.call( this );
console.log( greeting );
}
var me = {
name: "Kyle"
};
var you = {
name: "Reader"
};
identify.call( me ); // KYLE
identify.call( you ); // READER
speak.call( me ); // Hello, I'm KYLE
speak.call( you ); // Hello, I'm READER
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify( context );
console.log( greeting );
}
var me = {
name: "Kyle"
};
var you = {
name: "Reader"
};
identify( you ); // READER
speak( me ); // Hello, I'm KYLE

虽然两段代码实现的效果是一样的,但是有this的存在代码会更简洁。尤其是在使用模式更复杂的情况下,越能深刻的感受到这一点。所以这就是我们要学会使用this的原因。

困惑

它自己

对于this的第一种误解就是:它指向函数自己(我最开始就是这么认为的)。

用一个栗子来解释这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function foo(num) {
console.log( "foo: " + num );
// 追踪 `foo` 被调用了多少次
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// `foo` 被调用了多少次?
console.log( foo.count ); // 0 喵喵喵?

这段代码的原意是想要追踪foo被调用了多少次。但是,最后的foo.count却没有发生变化,依然为0

其中,foo.countfoo添加了一个count属性。但是,this.count并没有指向这个添加的count,即使属性名称相同,但它们的根对象是不同的。

这是我们可以用两种方式来解决这一问题:

第一种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function foo(num) {
console.log( "foo: " + num );
// 追踪 `foo` 被调用了多少次
data.count++;
}
var data = {
count: 0
};
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// `foo` 被调用了多少次?
console.log( data.count ); // 4

通过建立一个全局变量,让函数foo改变全局变量中持有的count。虽然解决了问题,但是它回避了this

第二种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function foo(num) {
console.log( "foo: " + num );
// 追踪 `foo` 被调用了多少次
foo.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// `foo` 被调用了多少次?
console.log( foo.count ); // 4

第三种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function foo(num) {
console.log( "foo: " + num );
// 追踪 `foo` 被调用了多少次
// 注意:由于 `foo` 的被调用方式(见下方),`this` 现在确实是 `foo`
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
// 使用 `call(..)`,我们可以保证 `this` 指向函数对象(`foo`)
foo.call( foo, i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// `foo` 被调用了多少次?
console.log( foo.count ); // 4

但是上面三种方法都回避了this

它的作用域

对于this的第二种误解就是:它指向函数的作用域。

参考下面的代码:

1
2
3
4
5
6
7
8
9
10
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo(); //undefined

写下这段代码的开发者试图用 thisfoo()bar() 的词法作用域间建立一座桥,使得bar() 可以访问 foo()内部作用域的变量 a这样的桥是不可能的。 你不能使用 this 引用在词法作用域中查找东西。这是不可能的。

每当你感觉自己正在试图使用 this 来进行词法作用域的查询时,提醒你自己:这里没有桥

什么是this

this 不是编写时绑定,而是运行时绑定。它依赖于函数调用的上下文条件。this 绑定与函数声明的位置没有任何关系,而与函数被调用的方式紧密相连。

第二章:this豁然开朗

调用点(Call-site)

调用点(call-site):函数在代码中被调用的位置(不是被声明的位置);

调用栈(call-stack):使我们到达当前执行位置而被调用的所有方法的堆栈。

关于调用点和调用栈的栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function baz() {
// 调用栈是: `baz`
// 我们的调用点是 global scope(全局作用域)
console.log( "baz" );
bar(); // <-- `bar` 的调用点
}
function bar() {
// 调用栈是: `baz` -> `bar`
// 我们的调用点位于 `baz`
console.log( "bar" );
foo(); // <-- `foo` 的 call-site
}
function foo() {
// 调用栈是: `baz` -> `bar` -> `foo`
// 我们的调用点位于 `bar`
console.log( "foo" );
}
baz(); // <-- `baz` 的调用点

可以通过console.trace()在控制台中查看调用栈。

四种规则

下面介绍判断this指向的四种判断规则:

默认绑定(Default Binding)

这是函数调用最常用的情况:独立函数调用。也可以认为这种this规则是在没有其他规则适用时的默认规则。

当函数是 一个直白,毫无修饰的调用时,即默认绑定时,有两种情况:

1、foo内容没在strict mode下,this指向全局变量。

2、foo内容在strict mode下,this将被设置为undefined

1
2
3
4
5
6
7
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2

此时的this实施了默认绑定,使this指向了全局变量。所以this.a引用的是全局变量中的a

但是如果strict mode在这里生效,name对于默认绑定来说是不合法的。此时的this将被设置为undefined

1
2
3
4
5
6
7
8
9
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: `this` is `undefined`

一个微妙但是重要的细节是:即便所有的 this 绑定规则都是完全基于调用点的,但如果 foo()内容 没有在 strict mode下执行,对于 默认绑定 来说全局对象是 唯一 合法的;foo() 的调用点的 strict mode 状态与此无关。

1
2
3
4
5
6
7
8
9
10
11
function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})();

隐含绑定(Implicit Binding)

举个栗子:

1
2
3
4
5
6
7
8
9
10
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2

观察这段代码会发现,foo()函数是在obj的环境下调用的,所以它的调用点是在obj对象这里。因此this就很自然的绑定在obj对象上。

在看一个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42

书上解释这一段代码的时候说:只有对象属性引用链的最后一层是影响调用点的。

其实,原理也很简单,因为foo()函数是在obj2环境下被调用的,所以它的调用点在obj2这里。自然而然的this.a就和obj2.a等价了,我么判断这种隐含绑定的时候关注调用点就对了。

隐含丢失(Implicitly Lost)

当隐含绑定丢失时,这通常以为着this会回退到默认绑定。然后根据strict mode存在与否来判断,this的指向是全局对象还是undefined

第一种:调用点的改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数引用!
var a = "oops, global"; // `a` 也是一个全局对象的属性
bar(); // "oops, global" // 调用点

因为函数bar()真正的 调用点没有任何其他的修饰所以它的this就为默认绑定。

第二种:传递一个回调函数时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// `fn` 只不过 `foo` 的另一个引用
fn(); // <-- 调用点!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // `a` 也是一个全局对象的属性
doFoo( obj.foo ); // "oops, global"

其实就可以理解为:回调函数导致了函数调用点的变化。

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // `a` 也是一个全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"

setTimeout函数展开为下面形式,即js环境内建的实现,就很好理解了:

1
2
3
4
function setTimeout(fn,delay) {
// (通过某种方法)等待 `delay` 毫秒
fn(); // <-- 调用点!
}

明确绑定(Explicit Binding)

从上述的隐含绑定中可以看出这种绑定都是隐性的,下面我们就要看到一个很直接的绑定方式。

其实前面我们就有接触了,call()apply()就是明确绑定很好的实现方式。

它们接收的第一个参数都是一个用于 this 的对象,之后使用这个指定的 this 来调用函数。因为你已经直接指明你想让 this 是什么,所以我们称这种方式为 明确绑定(explicit binding)

1
2
3
4
5
6
7
8
9
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
foo.call( obj ); // 2

不幸的是,单独依靠 明确绑定 仍然不能为我们先前提到的问题提供解决方案,也就是函数“丢失”自己原本的 this 绑定,或者被第三方框架覆盖,等等问题。

硬绑定(Hard Binding)

明确绑定的一种变种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
var obj2 = {
a: 3
}
// `bar` 将 `foo` 的 `this` 硬绑定到 `obj`
// 所以它不可以被覆盖
bar.call( obj2 ); // 2

new 绑定(new Binding)

当在函数前面被加入 new 调用时,也就是构造器调用时,下面这些事情会自动完成:

  1. 一个全新的 对象会凭空创建(就是被构建)
  2. 这个新构建的对象会被接入原形链([[Prototype]]-linked)
  3. 这个新构建的对象被设置为函数调用的 this 绑定
  4. 除非函数返回一个它自己的其他 对象,否则这个被 new 调用的函数将 自动 返回这个新构建的对象。

注意:new构建的是一个全新的对象而不是一个函数,是将构造器中的this绑定到新的对象上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo(a) {
this.a = a;
}
var bar = new foo( 2 );
console.log( bar.a ); // 2
对比
function foo(a) {
this.a = a;
}
var bar = foo( 2 );
console.log( bar.a ); // TypeError

通过在前面使用 new 来调用 foo(..),我们构建了一个新的对象并把这个新对象作为 foo(..) 调用的 thisnew 是函数调用可以绑定 this 的最后一种方式,我们称之为 new 绑定(new binding)

一切皆有顺序

比较隐含绑定和明确绑定哪一个优先?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2

明确绑定优先于隐含绑定

比较硬绑定与new绑定的优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar( 3 );
console.log( obj1.a ); // 2
console.log( baz.a ); // 3

参考“山寨”绑定帮助函数

1
2
3
4
5
function bind(fn, obj) {
return function() {
fn.apply( obj, arguments );
};
}

根据上面的代码我们会发现:new绑定无法将硬绑定时的obj1对象覆盖。所以硬绑定的优先级要大于new绑定。

new可以覆盖硬绑定一种情况:

1
2
3
4
5
6
7
8
9
10
11
12
function foo(p1,p2) {
this.val = p1 + p2;
}
// 在这里使用 `null` 是因为在这种场景下我们不关心 `this` 的硬绑定
// 而且反正它将会被 `new` 调用覆盖掉!
// 这种方式称为bind的柯里化
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" );
baz.val; // p1p2

判定this

  1. 函数是通过 new 被调用的吗(new 绑定)?如果是,this 就是新构建的对象。

    var bar = new foo()

  2. 函数是通过 callapply 被调用(明确绑定),甚至是隐藏在 bind 硬绑定 之中吗?如果是,this 就是那个被明确指定的对象。

    var bar = foo.call( obj2 )

  3. 函数是通过环境对象(也称为拥有者或容器对象)被调用的吗(隐含绑定)?如果是,this 就是那个环境对象。

    var bar = obj1.foo()

  4. 否则,使用默认的 this默认绑定)。如果在 strict mode 下,就是 undefined,否则是 global 对象。

    var bar = foo()

以上,就是理解对于普通的函数调用来说的 this 绑定规则 所需的全部。是的……几乎是全部。

绑定的特例

主要介绍规则的一些例外。

被忽略的this

如果你传递 nullundefined 作为 callapplybindthis 绑定参数,那么这些值会被忽略掉,取而代之的是 默认绑定 规则将适用于这个调用。

1
2
3
4
5
6
7
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2

使用这种做法的原因:

  • 使用apply(...)来展开一个数组,作为函数调用的参数;
  • bind(...)可以柯里化参数(增加预设值)
1
2
3
4
5
6
7
8
9
10
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 将数组散开作为参数
foo.apply( null, [2, 3] ); // a:2, b:3
// 用 `bind(..)` 进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3

注意:ES6中已经有一个扩展操作符:...,它可以让你无需使用 apply(..) 而在语法上将一个数组“散开”作为参数,比如 foo(...[1,2]) 表示 foo(1,2)

可是,在你不关心 this 绑定而一直使用 null 的时候,有些潜在的“危险”。如果你这样处理一些函数调用(比如,不归你管控的第三方包),而且那些函数确实使用了 this 引用,那么 默认绑定 规则意味着它可能会不经意间引用(或者改变,更糟糕!)global 对象(在浏览器中是 window)。

更安全的this

更安全的做法是创建一个‘DMZ’(非军事区)对象——只不过是一个完全为空。没有委托的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 我们的 DMZ 空对象
var ø = Object.create( null );
// 将数组散开作为参数
foo.apply( ø, [2, 3] ); // a:2, b:3
// 用 `bind(..)` 进行 currying
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3

间接

创建对函数的“间接引用”。

常见的间接引用产生方式是通过赋值:

1
2
3
4
5
6
7
8
9
10
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2

赋值表达式 p.foo = o.fooa的值到底为什么我们应该关心函数foo的调用点, p.foo = o.foo的作用是在对象p中创建了一个foo:foo,而真正调用foo的是在全局下。所以,输出值理所为2

软化绑定

我们之前看到 硬绑定 是一种通过将函数强制绑定到特定的 this 上,来防止函数调用在不经意间退回到 默认绑定的策略(除非你用 new 去覆盖它!)。问题是,硬绑定 极大地降低了函数的灵活性,阻止我们手动使用 隐含绑定或后续的 明确绑定 来覆盖 this

如果有这样的办法就好了:为 默认绑定 提供不同的默认值(不是 globalundefined),同时保持函数可以通过 隐含绑定明确绑定 技术来手动绑定 this

我们可以构建一个所谓的 软绑定 工具来模拟我们期望的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this,
curried = [].slice.call( arguments, 1 ),
bound = function bound() {
return fn.apply(
(!this ||
(typeof window !== "undefined" &&
this === window) ||
(typeof global !== "undefined" &&
this === global)
) ? obj : this,
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}

这里提供的 softBind(..) 工具的工作方式和 ES5 内建的 bind(..) 工具很相似,除了我们的 软绑定 行为。它用一种逻辑将指定的函数包装起来,这个逻辑在函数调用时检查 this,如果它是 globalundefined,就使用预先指定的 默认值obj),否则保持 this 不变。它也提供了可选的柯里化行为(见先前的 bind(..) 讨论)。

我们来看看它的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo() {
console.log("name: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!
fooOBJ.call( obj3 ); // name: obj3 <---- 看!
setTimeout( obj2.foo, 10 ); // name: obj <---- 退回到软绑定

软绑定版本的 foo() 函数可以如展示的那样被手动 this 绑定到 obj2obj3,如果 默认绑定 适用时会退到 obj

词法this

箭头函数与使用四种标准的this规则不同的是,箭头函数从封闭它的(函数或全局)作用域采用this绑定。即,箭头函数以声明它的函数为它的全局,再利用四种规则来决定this的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo() {
// 返回一个箭头函数
return (a) => {
// 这里的 `this` 是词法上从 `foo()` 采用的
console.log( this.a );
};
}
var obj1 = {
a: 2
};
var obj2 = {
a: 3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是3!

产生上述结果的原因是:一个箭头函数的此法绑定是不能被覆盖的(就连new也不行)。

ES6之前的一种引用形式:

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
var self = this; // 词法上捕获 `this`
setTimeout( function(){
console.log( self.a );
}, 100 );
}
var obj = {
a: 2
};
foo.call( obj ); // 2

在编码时应该统一风格,词法和this不应该混用。

第三章:对象

语法

字面语法:

1
2
3
4
var myObj = {
key: value
// ...
};

构造形式:

1
2
var myObj = new Object();
myObj.key = value;

类型

js的七种基本数据类型:

  • string
  • number
  • boolean
  • null
  • undefined
  • object
  • Symbol

前五种基本类型自身不是objectnull有时会被当成一个对象类型。typeof(null)会返回object,实际山,null是它自己的基本类型。

一个常见的错误论断是“JavaScript中的一切都是对象”。这明显是不对的

function和数组都是对象。

内建对象

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

这些内建函数的每一个都可以被当做构造器。

1
2
3
4
5
6
7
8
9
10
var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
var strObject = new String( "I am a string" );
typeof strObject; // "object"
strObject instanceof String; // true
// 考察 object 子类型
Object.prototype.toString.call( strObject ); // [object String]

但更推荐使用字面形式的值,而非构造的对象形式。

1
2
3
4
5
var strPrimitive = "I am a string";
console.log( strPrimitive.length ); // 13
console.log( strPrimitive.charAt( 3 ) ); // "m"

内容

属性访问和键访问

1
2
3
4
5
6
7
var myObject = {
a: 2
};
myObject.a; // 2 // 属性访问
myObject["a"]; // 2 // 键访问

属性访问:.操作符后面需要一个标识符兼容的属性名;

键访问:[]中可以接受任何兼容UTF-8/unicode的字符串作为属性名。

例:名为“Super-Fun!”的属性,不得不使用["Super-Fun"]语法访问。

[]中还可以跟一个变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var wantA = true;
var myObject = {
a: 2
};
var idx;
if (wantA) {
idx = "a";
}
// 稍后
console.log( myObject[idx] ); // 2

[]中的属性名总是字符串,不要将对象和数组使用的数字搞混:

1
2
3
4
5
6
7
8
9
var myObject = { };
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"

计算型属性名

1
2
3
4
5
6
7
8
9
var prefix = "foo";
var myObject = {
[prefix + "bar"]: "hello",
[prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world

ES6中添加的新的数据类型Symbol的应用:

1
2
3
var myObject = {
[Symbol.Something]: "hello world"
};

属性vs.方法

每次你访问一个对象的属性都是一个 属性访问,无论你得到什么类型的值。如果你 恰好 从属性访问中得到一个函数,它也没有魔法般地在那时成为一个“方法”。一个从属性访问得来的函数没有任何特殊性(隐含的 this 绑定的情况在刚才已经解释过了)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {
console.log( "foo" );
}
var someFoo = foo; // 对 `foo` 的变量引用
var myObject = {
someFoo: foo
};
foo; // function foo(){..}
someFoo; // function foo(){..}
myObject.someFoo; // function foo(){..}

someFoomyObject.someFoo 只不过是同一个函数的两个分离的引用,它们中的任何一个都不意味着这个函数很特别或被其他对象所“拥有”。如果上面的 foo() 定义里面拥有一个 this 引用,那么 myObject.someFoo隐含绑定 将会是这个两个引用间 唯一 可以观察到的不同。它们中的任何一个都没有称为“方法”的道理。

数组

数组也是对象。所以虽然每个索引都是正整数,还是可以在数组上添加属性:

1
2
3
4
5
6
7
var myArray = [ "foo", 42, "bar" ];
myArray.baz = "baz";
myArray.length; // 3
myArray.baz; // "baz"

注意:添加命名属性(不论是使用.还是[]操作符语法)不会改变数组length的值。

小心: 如果你试图在一个数组上添加属性,但是属性名 看起来 像一个数字,那么最终它会成为一个数字索引(也就是改变了数组的内容):

1
2
3
4
5
6
7
var myArray = [ "foo", 42, "bar" ];
myArray["3"] = "baz";
myArray.length; // 4
myArray[3]; // "baz"

(?)复制对象

栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function anotherFunction() { /*..*/ }
var anotherObject = {
c: true
};
var anotherArray = [];
var myObject = {
a: 2,
b: anotherObject, // 引用,不是拷贝!
c: anotherArray, // 又一个引用!
d: anotherFunction
};
anotherArray.push( anotherObject, myObject );

浅拷贝:得到一个新的对象,a是值2的拷贝,但bcd属性仅仅是引用;

深拷贝:不仅复制myobject,还会复制anotherObjectanotherArray,就会得到一个无限循环的问题。

浅拷贝易懂,所以ES6为此任务定义了Object.assign(...)

1
2
3
4
5
6
var newObj = Object.assign( {}, myObject );
newObj.a; // 2
newObj.b === anotherObject; // true
newObj.c === anotherArray; // true
newObj.d === anotherFunction; // true

属性描述符

查看属性描述:

1
2
3
4
5
6
7
8
9
10
11
var myObject = {
a: 2
};
Object.getOwnPropertyDescriptor( myObject, "a" ); // 在myObject对象 中,查看a值的属性
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }

定义属性描述:

1
2
3
4
5
6
7
8
9
10
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
} );
myObject.a; // 2

可写性(writable)

即表示该属性值能否被更改:

1
2
3
4
5
6
7
8
9
10
11
12
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // 不可写!
configurable: true,
enumerable: true
} );
myObject.a = 3;
myObject.a; // 2

strict mode下:

1
2
3
4
5
6
7
8
9
10
11
12
"use strict";
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // 不可写!
configurable: true,
enumerable: true
} );
myObject.a = 3; // TypeErro

可配置型(Configurable)

即是否能定义属性描述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var myObject = {
a: 2
};
myObject.a = 3;
myObject.a; // 3
Object.defineProperty( myObject, "a", {
value: 4,
writable: true,
configurable: false, // 不可配置!
enumerable: true
} );
myObject.a; // 4
myObject.a = 5;
myObject.a; // 5
Object.defineProperty( myObject, "a", {
value: 6,
writable: true,
configurable: true,
enumerable: true
} ); // TypeError

注意:这里直接导致了TypeError,与strict mode无关。

注意:将configurable设置为false是一个单项操作,不可撤销!

configurable:false 阻止的另外一个事情是使用 delete 操作符移除既存属性的能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var myObject = {
a: 2
};
myObject.a; // 2
delete myObject.a;
myObject.a; // undefined
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: false,
enumerable: true
} );
myObject.a; // 2
delete myObject.a;
myObject.a; // 2

如你所见,最后的 delete 调用(无声地)失败了,因为我们将 a 属性设置成了不可配置。

delete 仅用于直接从目标对象移除该对象的(可以被移除的)属性。如果一个对象的属性是某个其他对象/函数的最后一个现存的引用,而你 delete 了它,那么这就移除了这个引用,于是现在那个没有被任何地方所引用的对象/函数就可以被作为垃圾回收。但是,将 delete 当做一个像其他语言(如 C/C++)中那样的释放内存工具是 恰当的。delete 仅仅是一个对象属性移除操作 —— 没有更多别的含义。

可枚举性(Enumerable)

即属性呢能否在特定对象-属性枚举操作中出现,比如for...in循环。

不可变性(Immutability)

即属性或对象的不可变性。

ES5实现不可变性的方法都只是实现了浅不可变性。也就是,他们仅影响对象和它的直属属性的性质。如果对象拥有对其他对象(数组、对象、函数等)的引用,那个对象的内容就不会受影响。

1
2
3
myImmutableObject.foo; // [1,2,3]
myImmutableObject.foo.push( 4 );
myImmutableObject.foo; // [1,2,3,4]

对象常量(Object Constant)

writable:falseconfigurable组合,可以在实质上创建一个作为对象属性的常量,如:

1
2
3
4
5
6
7
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
} );

防止扩展(Prevent Extensions)

防止对象添加新的属性,保留既存的对象属性。调用Object.preventExtensions(...)

1
2
3
4
5
6
7
8
var myObject = {
a: 2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined

在非strict mode模式下,b的创建无声地失效;在strict mode模式下,会抛出TypeError

封印(Seal)

Object.seal(...)创建一个封印对象,相当于在实质上调用Object.preventExtensions(...)的同时,将它所有既存属性标记为configurable:false

因此,既不能添加属性,也不能重新配置或删除既存属性(虽然你依然 可以 修改它们的值)。

(?)冻结(Freeze)

Object.freeze(..) 创建一个冻结的对象,这意味着它实质上在当前的对象上调用 Object.seal(..),同时也将它所有的“数据访问”属性设置为 writable:false,所以它们的值不可改变。

这种方法是你可以从对象自身获得的最高级别的不可变性,因为它阻止任何对对象或对象直属属性的改变(虽然,就像上面提到的,任何被引用的对象的内容不受影响)。

你可以“深度冻结”一个对象:在这个对象上调用 Object.freeze(..),然后递归地迭代所有它引用的(目前还没有受过影响的)对象,然后也在它们上面调用 Object.freeze(..)。但是要小心,这可能会影响其他你并不打算影响的(共享的)对象。

[[Get]]

关于属性访问如何工作:

1
2
3
4
5
var myObject = {
a: 2
};
myObject.a; // 2

根据语言规范,上面的代码实际上在 myObject 上执行了一个 [[Get]] 操作(有些像 [[Get]]() 函数调用)。对一个对象进行默认的内建 [[Get]] 操作,首先检查对象,寻找一个拥有被请求的名称的属性,如果找到,就返回相应的值。

然而,如果按照被请求的名称 没能 找到属性,[[Get]] 的算法定义了另一个重要的行为。遍历 [[Prototype]] 链,如果有的话。

[[Get]] 操作的一个重要结果是,如果它通过任何方法都不能找到被请求的属性的值,那么它会返回 undefined

1
2
3
4
5
var myObject = {
a: 2
};
myObject.b; // undefined

这个行为和你通过标识符名称来引用 变量 不同。如果你引用了一个在可用的词法作用域内无法解析的变量,其结果不是像对象属性那样返回 undefined,而是抛出一个 ReferenceError

1
2
3
4
5
6
7
var myObject = {
a: undefined
};
myObject.a; // undefined
myObject.b; // undefined

的角度来说,这两个引用没有区别 —— 它们的结果都是 undefined。然而,在 [[Get]] 操作的底层,虽然不明显,但是比起处理引用 myObject.a,处理 myObject.b 的操作要多做一些潜在的“工作”。

如果仅仅考察结果的值,你无法分辨一个属性是存在并持有一个 undefined 值,还是因为属性根本 存在所以 [[Get]] 无法返回某个具体值而返回默认的 undefined。但是,你很快就能看到你其实 可以 分辨这两种场景。

[[Put]]

它和[[Get]]是存在微妙不同的:

调用 [[Put]] 时,它根据几个因素表现不同的行为,包括(影响最大的)属性是否已经在对象中存在了。

如果属性存在,[[Put]] 算法将会大致检查:

  1. 这个属性是访问器描述符吗(见下一节”Getters 与 Setters”)?如果是,而且是 setter,就调用 setter。
  2. 这个属性是 writablefalse 数据描述符吗?如果是,在非 strict mode 下无声地失败,或者在 strict mode 下抛出 TypeError。
  3. 否则,像平常一样设置既存属性的值。

如果属性在当前的对象中还不存在,[[Put]] 操作会变得更微妙和复杂。我们将在第五章讨论 [[Prototype]] 时再次回到这个场景,更清楚地解释它。

Getters与Setters

Getter:调用一个隐藏函数来取得值的属性;Setter:调用一个隐藏函数来设置值的属性。

当一个属性被定义为拥有getter或setter,那么它们的定义就成了“访问描述符”(与“数据描述符”相对)。

访问描述符的 valuewritable 性质因没有意义而被忽略,取而代之的是 JS 将会考虑属性的 setget 性质(还有 configurableenumerable)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var myObject = {
// 为 `a` 定义一个 getter
get a() {
return 2;
}
};
Object.defineProperty(
myObject, // 目标对象
"b", // 属性名
{ // 描述符
// 为 `b` 定义 getter
get: function(){ return this.a * 2 },
// 确保 `b` 作为对象属性出现
enumerable: true
}
);
myObject.a; // 2
myObject.b; // 4
1
2
3
4
5
6
7
8
9
10
var myObject = {
// 为 `a` 定义 getter
get a() {
return 2;
}
};
myObject.a = 3;
myObject.a; // 2 没有定义setter,赋值操作无意义

定义setter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var myObject = {
// 为 `a` 定义 getter
get a() {
return this._a_; // _a_ 只是例子中的单纯惯例,无特别之处
},
// 为 `a` 定义 setter
set a(val) {
this._a_ = val * 2;
}
};
myObject.a = 2;
myObject.a; // 4

存在性(Existence)

myObject.a 属性访问会得到一个undefined值,怎么区别它是存储着undefined还是没有被定义:

1
2
3
4
5
6
7
8
9
var myObject = {
a: 2
};
("a" in myObject); // true 检查属性是否存在对象中
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true 仅检查·myObject·是否拥有属性,不会查询 [[Prototype]]链
myObject.hasOwnProperty( "b" ); // false

对于第二种方式更健壮的方式:

object.prototype.hasOwnProperty.call(myObject,"a")

枚举(Enumeration)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var myObject = { };
Object.defineProperty(
myObject,
"a",
// 使 `a` 可枚举,如一般情况
{ enumerable: true, value: 2 }
);
Object.defineProperty(
myObject,
"b",
// 使 `b` 不可枚举
{ enumerable: false, value: 3 }
);
myObject.b; // 3
("b" in myObject); // true 注意!!
myObject.hasOwnProperty( "b" ); // true
// .......
for (var k in myObject) { // for...in循环来遍历对象中的属性
console.log( k, myObject[k] );
}
// "a" 2
// 因为 “enumerable” 基本上意味着“如果对象的属性被迭代时会被包含在内”。

另一个可以区分可枚举和不可枚举属性的方法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var myObject = { };
Object.defineProperty(
myObject,
"a",
// 使 `a` 可枚举,如一般情况
{ enumerable: true, value: 2 }
);
Object.defineProperty(
myObject,
"b",
// 使 `b` 不可枚举
{ enumerable: false, value: 3 }
);
myObject.propertyIsEnumerable( "a" ); // true
myObject.propertyIsEnumerable( "b" ); // false
Object.keys( myObject ); // ["a"]
Object.getOwnPropertyNames( myObject ); // ["a", "b"]

(*)迭代(Iteration)

几种迭代方法:

forEach(...):用于迭代数组中的值,忽略回调返回值

1
2
3
4
5
var myArray = [1, 2, 3];
myArray.forEach(function (item) {
console.log(item);
});

every(...):一直迭代到最后,或回调返回一个false

1
2
3
4
5
6
const isBiggerThan10 = (element, index, array) => {
return element > 10;
};
[1,23,3,4,5].every(isBiggerThan10); // false
[11,12,13,14].every(isBiggerThan10); // true

some(...):一直迭代到最后,或回调返回一个true

1
2
3
4
5
6
const isBiggerThan10 = (element, index, array) => {
return element > 10;
};
[1,23,3,4,5].some(isBiggerThan10); // true
[1,2,3,4,5].some(isBiggerThan10); // false

for...of循环:用来迭代数组和有迭代器的对象

1
2
3
4
var myArray = [1,2,3,4];
for (var v of myArray) {
console.log(v);
} // 用来迭代数组和带迭代器的对象

关于迭代器@@iterator它本身不是迭代器对象,而是一个返回迭代器对象的方法

数组中拥有内建的迭代器。对象中没有。

使用内建的@@iterator手动迭代数组:

1
2
3
4
5
6
7
var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator](); // Symbol为ES6中
it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { done:true } // 使自己知道迭代已完成

为想要的迭代对象定义自己的默认@@iterator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var myObject = {
a: 2,
b: 3
};
Object.defineProperty( myObject, Symbol.iterator, {
enumerable: false,
writable: false,
configurable: true,
value: function() {
var o = this;
var idx = 0;
var ks = Object.keys( o );
return {
next: function() {
return {
value: o[ks[idx++]],
done: (idx > ks.length)
};
}
};
}
} );
// 手动迭代 `myObject`
var it = myObject[Symbol.iterator]();
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { value:undefined, done:true }
// 用 `for..of` 迭代 `myObject`
for (var v of myObject) {
console.log( v );
}
// 2
// 3

一个“无穷”迭代器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var randoms = {
[Symbol.iterator]: function() {
return {
next: function() {
return { value: Math.random() };
}
};
}
};
var randoms_pool = [];
for (var n of randoms) {
randoms_pool.push( n );
// 不要超过边界!
if (randoms_pool.length === 100) break;
}

第四章:混合(淆)“类”的对象

讨论:面向对象(oo)编程,类(class):实例化、继承、多态

类理论

(略)哈哈哈哈哈哈哈哈

第五章:原型(Prototype)

[[Prototype]]

[[Prototype]]是对象的一个内部属性,它是一个其他对象的引用。对象被创建时,这个属性被赋予了一个非null值。

一个对象拥有一个空的[[Prototype]]链接是可能的

1
2
3
4
5
var myObject = {
a: 2
};
myObject.a; // 2

myObject.a操作用到了[[Get]]

第一步:检查对象本身是否有一个a属性,如果有就使用它;

第二步:当没有在对象本身找到时,就沿着[[Prototype]]链继续往下找,直到找到该属性,或者原型链为空返回undefined

使用[[Prototype]]

1
2
3
4
5
6
7
8
var anotherObject = {
a: 2
};
// 创建一个链接到 `anotherObject` 的对象
var myObject = Object.create( anotherObject );
myObject.a; // 2

对于for...in循环,它也会像查询一样,枚举链条上可以找到的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
var anotherObject = {
a: 2
};
// 创建一个链接到 `anotherObject` 的对象
var myObject = Object.create( anotherObject );
for (var k in myObject) {
console.log("found: " + k);
}
// 找到: a
("a" in myObject); // true

Object.prototype

[[Prototype]]链在Object.prototype处终结,即每个普通的 [[Prototype]] 链的最顶端,是内建的 Object.prototype

js中所有普通的对象都“衍生自”Object.prototype对象。

设置与遮蔽属性

1
myObject.foo = "bar";

进行上述操作会出现四种情况:

1、当foo属性不直接存在myObject上,也不存在myObject[[Prototype]]链的更高层时。foo作为一个新的属性被添加到myObject上。

foo 不直接存在myObject,但 存在myObject[[Prototype]] 链的更高层时:

2、如果一个普通的名为 foo 的数据访问属性在 [[Prototype]] 链的高层某处被找到,而且没有被标记为只读(writable:false),那么一个名为 foo 的新属性就直接添加到 myObject 上,形成一个 遮蔽属性

3、如果一个 foo[[Prototype]] 链的高层某处被找到,但是它被标记为 只读(writable:false) ,那么设置既存属性和在 myObject 上创建遮蔽属性都是 不允许 的。如果代码运行在 strict mode 下,一个错误会被抛出。否则,这个设置属性值的操作会被无声地忽略。不论怎样,没有发生遮蔽

4、如果一个 foo[[Prototype]] 链的高层某处被找到,而且它是一个 setter(见第三章),那么这个 setter 总是被调用。没有 foo 会被添加到(也就是遮蔽在)myObject 上,这个 foo setter 也不会被重定义。

如果想在第三种和第四种情况下遮蔽foo,就不能使用=赋值,而使用Object.defineProperty(...)foo添加到myObject

注意下面这一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var anotherObject = {
a: 2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false
myObject.a++; // 噢,隐式遮蔽!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true

“类”

“类函数”

在js中,所有的函数默认都会得到一个公有的,不可枚举的属性,称为prototype,它可以指向任意的对象。

1
2
3
4
5
6
7
function Foo() {
console.log('hello');
}
Foo.prototype; // {constructor: ƒ} 这个对象经常被称为“Foo的原型”。
// constructor: f foo()
// _proto_: Object

每个由调用new Foo()而创建的对象将最终被[[Prototype]]链接到这个Foo.prototype对象。

即:

1
2
3
4
5
6
7
function Foo() {
// ...
}
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true

通过new Foo()创建a时,发生的事情之一是a得到一个内部[[Prototype]]链接,此链接链到Foo.prototype所指向的对象。

所以我们可以看到,在new Foo()这一过程我们并没有做任何从一个类到一个实体对象的拷贝,我们只是将两个对象互相链接在了一起。

“构造器”(Constructors)

1
2
3
4
5
6
7
8
function Foo() {
// ...
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; // true

Foo.prototype对象默认得到一个公有的.constructor属性,这个属性是不可枚举的并且指向Foo

a.constructor === Foo;虽然为true,但a并没有拥有.constructor

构造器还是调用?

1
2
3
4
5
6
7
8
function NothingSpecial() {
console.log( "Don't mind me!" );
}
var a = new NothingSpecial();
// "Don't mind me!"
a; // NothingSpecial {}

结论:函数自身不是构造器,当且仅当被new调用时,函数调用是一个“构造器调用”。

机制

1
2
3
4
5
6
7
8
9
10
11
12
13
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
var a = new Foo( "a" );
var b = new Foo( "b" );
a.myName(); // "a"
b.myName(); // "b"

分析:

ab的创建:通过newFoo中的this分别绑定到ab上,因此在ab上分别添加了name属性。

两个返回值:根据ab创建的原理可知,他们都被链接到了Foo.prototype 上,所以根据[[Get]]的原理,会得到“a”和”b“这两个输出值。

复活“构造器”

1
2
3
4
5
6
7
8
9
function Foo() { /* .. */ }
Foo.prototype = {
construcor: "hello"
}; // 创建一个新的 prototype 对象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!

分析:

首先我们要明白:默认的Foo.prototype中,含有一个constructor: f Foo()

所以,当我们运行a1.constructor === Foo;时,并不是a1中含有construtor这个属性而是根据[[Get]]Foo.prototype中得到。

然后通过新建对象,改变了constructor的值。

因此当运行a1.constructor === Object;会返回true,因为根据[[Get]]的原理,我们会在Object.prototype中找到constructor: Object

误解,消除!

.constructor 加回到 Foo.prototype 对象上,并且具有原生行为中的不可枚举性:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新的 prototype 对象
// 需要正确地“修复”丢失的 `.construcor`
// 新对象上的属性以 `Foo.prototype` 的形式提供。
// `defineProperty(..)` 的内容见第三章。
Object.defineProperty( Foo.prototype, "constructor" , {
enumerable: false,
writable: true,
configurable: true,
value: Foo // 使 `.constructor` 指向 `Foo`
} );

“(原型)继承”

img

完成上述图中链接方式的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
function Bar(name,label) {
Foo.call( this, name );
this.label = label;
}
// 这里,我们创建一个新的 `Bar.prototype` 链接链到 `Foo.prototype`
Bar.prototype = Object.create( Foo.prototype );
// 注意!现在 `Bar.prototype.constructor` 不存在了,
// 如果你有依赖这个属性的习惯的话,它可以被手动“修复”。
Bar.prototype.myLabel = function() {
return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"

分析:

通过Bar.prototype = Object.create( Foo.prototype );创建一个新的Bar.prototype链接到Foo.prototype,并将原来错误的链对象扔掉。

下面两种方法也能工作,但和预期的不同:

1
2
3
4
5
6
// 不会如你期望的那样工作!
Bar.prototype = Foo.prototype;
// 会如你期望的那样工作
// 但会带有你可能不想要的副作用 :(
Bar.prototype = new Foo();

分析:

第一种方式:是将Bar直接连接到Foo.prototype上,而不是将Bar.prototype链接到Foo.prototype上,所以当运行 Bar.prototype.myLabel = ...时,实际上是修改的Foo.prototype对象本身。

第二种方式:利用构造器链接就很容易出现,Foo()中的this被绑定到了Bar.prototype这种情况。

对比ES6 之前和 ES6 标准的技术如何处理将 Bar.prototype 链接至 Foo.prototype

1
2
3
4
5
6
7
// ES6 以前
// 扔掉默认既存的 `Bar.prototype`
Bar.prototype = Object.create( Foo.prototype );
// ES6+
// 修改既存的 `Bar.prototype`
Object.setPrototypeOf( Bar.prototype, Foo.prototype );

考察“类”关系

主要讨论如何检验两个对象间是否有委托关系:

1
2
3
4
5
6
7
function Foo() {
// ...
}
Foo.prototype.blah = ...;
var a = new Foo();

第一种方式:

利用instanceofinstanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数prototype 属性。

1
a instanceof Foo; // true

第二种方式:(也是最简洁的方式)

1
Foo.prototype.isPrototypeOf( a ); // true
1
2
// 简单地:`b` 在 `c` 的 `[[Prototype]]` 链中出现过吗?
b.isPrototypeOf( c );
1
Object.getPrototypeOf( a );
1
Object.getPrototypeOf( a ) === Foo.prototype; // true
1
a.__proto__ === Foo.prototype; // true

.__proto__实际上不存在于a上,而是存在于内建的 Object.prototype 上。而且,.__proto__ 虽然看起来像一个属性,但实际上将它看做是一个 getter/setter(见第三章)更合适。

1
2
3
4
5
6
7
8
9
10
Object.defineProperty( Object.prototype, "__proto__", {
get: function() {
return Object.getPrototypeOf( this );
},
set: function(o) {
// ES6 的 setPrototypeOf(..)
Object.setPrototypeOf( this, o );
return o;
}
} );

对象链接

主要讲解Object.create(...)

用法:

1
2
3
4
5
6
7
8
9
var foo = {
something: function() {
console.log( "Tell me something good..." );
}
};
var bar = Object.create( foo );
bar.something(); // Tell me something good...

var bar = Object.create( foo );即创建一个链接到foo上的新对象bar

填补Object.create(...)

因为Object.create(...)是在ES5中引入的,所以要支持ES5之前的环境,就需要用以下方式来填补:

1
2
3
4
5
6
7
if (!Object.create) { // 判断是否支持Object.create
Object.create = function(o) {
function F(){}
F.prototype = o; // 将o赋值给F.prototype,所以新创建的函数就会链接到o
return new F();
};
}

另一种不可填补的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var anotherObject = {
a: 2
};
var myObject = Object.create( anotherObject, {
b: {
enumerable: false,
writable: true,
configurable: false,
value: 3
},
c: {
enumerable: true,
writable: false,
configurable: false,
value: 4
}
} );
myObject.hasOwnProperty( "a" ); // false
myObject.hasOwnProperty( "b" ); // true
myObject.hasOwnProperty( "c" ); // true
myObject.a; // 2
myObject.b; // 3
myObject.c; // 4

Object.create(..) 的第二个参数通过声明每个新属性的 属性描述符指定了要添加在新对象上的属性。

因为,属性描述符在ES5之前的环境是不可填补的,所以Object.create(..) 这一方法无法填补。

链接作为候补?

1
2
3
4
5
6
7
8
9
var anotherObject = {
cool: function() {
console.log( "cool!" );
}
};
var myObject = Object.create( anotherObject );
myObject.cool(); // "cool!"

如果引用上述方法是将anotherObject作为候补,来访问cool属性,那么上述方式是不提倡的,而应该用:

1
2
3
4
5
6
7
8
9
10
11
12
13
var anotherObject = {
cool: function() {
console.log( "cool!" );
}
};
var myObject = Object.create( anotherObject );
myObject.doCool = function() {
this.cool(); // internal delegation!
};
myObject.doCool(); // "cool!"

复习

toString()valueOf(),和其他几种共同工具都存在于这个 Object.prototype 对象上,这解释了语言中所有的对象是如何能够访问他们的。

那个用 new 调用的函数有一个被随便地命名为 .prototype 的属性,这个属性所引用的对象恰好就是这个新对象链接到的“另一个对象”。带有 new 的函数调用通常被称为“构造器”,尽管实际上它们并没有像传统的面向类语言那样初始化一个类。

第六章:行为委托

迈向面向委托设计

类理论

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Task {
id;
// `Task()` 构造器
Task(ID) { id = ID; }
outputTask() { output( id ); }
}
class XYZ inherits Task {
label;
// `XYZ()` 构造器
XYZ(ID,Label) { super( ID ); label = Label; } // 利用super来调用这一方法的泛化版本
outputTask() { super(); output( label ); }
}
class ABC inherits Task {
// ...
}

每个实例都拷贝了完成计划任务的所有行为。所以,在构建完成之后,你通常仅会与这些实例交互。

委托理论

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var Task = {
setID: function(ID) { this.id = ID; },
outputID: function() { console.log( this.id ); }
};
// 使 `XYZ` 委托到 `Task`
var XYZ = Object.create( Task );
XYZ.prepareTask = function(ID,Label) {
this.setID( ID );
this.label = Label;
};
XYZ.outputTaskDetails = function() {
this.outputID();
console.log( this.label );
};
XYZ.prepareTask(1, 2);
XYZ.outputTaskDetails(); // 1 2
// ABC = Object.create( Task );
// ABC ... = ...

作为与面向类(OO——面向对象)的对比,我们成上述代码为“OLOO”(链接到其他对象的对象))。

相互委托(不允许)

你不能在两个或多个对象间相互地委托(双向地)对方来创建一个 循环 。如果你使 B 链接到 A,然后试着让 A链接到 B,那么你将得到一个错误。